/**
 * Copyright (c) 2006-2015, JGraph Ltd
 * Copyright (c) 2006-2015, Gaudenz Alder
 */
/**
 * Class: mxSvgCanvas2D
 *
 * Extends <mxAbstractCanvas2D> to implement a canvas for SVG. This canvas writes all
 * calls as SVG output to the given SVG root node.
 *
 * (code)
 * var svgDoc = mxUtils.createXmlDocument();
 * var root = (svgDoc.createElementNS != null) ?
 * 		svgDoc.createElementNS(mxConstants.NS_SVG, 'svg') : svgDoc.createElement('svg');
 *
 * if (svgDoc.createElementNS == null)
 * {
 *   root.setAttribute('xmlns', mxConstants.NS_SVG);
 *   root.setAttribute('xmlns:xlink', mxConstants.NS_XLINK);
 * }
 * else
 * {
 *   root.setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:xlink', mxConstants.NS_XLINK);
 * }
 *
 * var bounds = graph.getGraphBounds();
 * root.setAttribute('width', (bounds.x + bounds.width + 4) + 'px');
 * root.setAttribute('height', (bounds.y + bounds.height + 4) + 'px');
 * root.setAttribute('version', '1.1');
 *
 * svgDoc.appendChild(root);
 *
 * var svgCanvas = new mxSvgCanvas2D(root);
 * (end)
 *
 * A description of the public API is available in <mxXmlCanvas2D>.
 *
 * To disable anti-aliasing in the output, use the following code.
 *
 * (code)
 * graph.view.canvas.ownerSVGElement.setAttribute('shape-rendering', 'crispEdges');
 * (end)
 *
 * Or set the respective attribute in the SVG element directly.
 *
 * Constructor: mxSvgCanvas2D
 *
 * Constructs a new SVG canvas.
 *
 * Parameters:
 *
 * root - SVG container for the output.
 * styleEnabled - Optional boolean that specifies if a style section should be
 * added. The style section sets the default font-size, font-family and
 * stroke-miterlimit globally. Default is false.
 */
function mxSvgCanvas2D(root, styleEnabled) {
  mxAbstractCanvas2D.call(this);

  /**
   * Variable: root
   *
   * Reference to the container for the SVG content.
   */
  this.root = root;

  /**
   * Variable: gradients
   *
   * Local cache of gradients for quick lookups.
   */
  this.gradients = [];

  /**
   * Variable: defs
   *
   * Reference to the defs section of the SVG document. Only for export.
   */
  this.defs = null;

  /**
   * Variable: styleEnabled
   *
   * Stores the value of styleEnabled passed to the constructor.
   */
  this.styleEnabled = styleEnabled != null ? styleEnabled : false;

  var svg = null;

  // Adds optional defs section for export
  if (root.ownerDocument != document) {
    var node = root;

    // Finds owner SVG element in XML DOM
    while (node != null && node.nodeName != "svg") {
      node = node.parentNode;
    }

    svg = node;
  }

  if (svg != null) {
    // Tries to get existing defs section
    var tmp = svg.getElementsByTagName("defs");

    if (tmp.length > 0) {
      this.defs = svg.getElementsByTagName("defs")[0];
    }

    // Adds defs section if none exists
    if (this.defs == null) {
      this.defs = this.createElement("defs");

      if (svg.firstChild != null) {
        svg.insertBefore(this.defs, svg.firstChild);
      } else {
        svg.appendChild(this.defs);
      }
    }

    // Adds stylesheet
    if (this.styleEnabled) {
      this.defs.appendChild(this.createStyle());
    }
  }
}

/**
 * Extends mxAbstractCanvas2D
 */
mxUtils.extend(mxSvgCanvas2D, mxAbstractCanvas2D);

/**
 * Capability check for DOM parser.
 */
(function () {
  mxSvgCanvas2D.prototype.useDomParser =
    !mxClient.IS_IE &&
    typeof DOMParser === "function" &&
    typeof XMLSerializer === "function";

  if (mxSvgCanvas2D.prototype.useDomParser) {
    // Checks using a generic test text if the parsing actually works. This is a workaround
    // for older browsers where the capability check returns true but the parsing fails.
    try {
      var doc = new DOMParser().parseFromString("test text", "text/html");
      mxSvgCanvas2D.prototype.useDomParser = doc != null;
    } catch (e) {
      mxSvgCanvas2D.prototype.useDomParser = false;
    }
  }
})();

/**
 * Variable: path
 *
 * Holds the current DOM node.
 */
mxSvgCanvas2D.prototype.node = null;

/**
 * Variable: matchHtmlAlignment
 *
 * Specifies if plain text output should match the vertical HTML alignment.
 * Defaul is true.
 */
mxSvgCanvas2D.prototype.matchHtmlAlignment = true;

/**
 * Variable: textEnabled
 *
 * Specifies if text output should be enabled. Default is true.
 */
mxSvgCanvas2D.prototype.textEnabled = true;

/**
 * Variable: foEnabled
 *
 * Specifies if use of foreignObject for HTML markup is allowed. Default is true.
 */
mxSvgCanvas2D.prototype.foEnabled = true;

/**
 * Variable: foAltText
 *
 * Specifies the fallback text for unsupported foreignObjects in exported
 * documents. Default is '[Object]'. If this is set to null then no fallback
 * text is added to the exported document.
 */
mxSvgCanvas2D.prototype.foAltText = "[Object]";

/**
 * Variable: foOffset
 *
 * Offset to be used for foreignObjects.
 */
mxSvgCanvas2D.prototype.foOffset = 0;

/**
 * Variable: textOffset
 *
 * Offset to be used for text elements.
 */
mxSvgCanvas2D.prototype.textOffset = 0;

/**
 * Variable: imageOffset
 *
 * Offset to be used for image elements.
 */
mxSvgCanvas2D.prototype.imageOffset = 0;

/**
 * Variable: strokeTolerance
 *
 * Adds transparent paths for strokes.
 */
mxSvgCanvas2D.prototype.strokeTolerance = 0;

/**
 * Variable: minStrokeWidth
 *
 * Minimum stroke width for output.
 */
mxSvgCanvas2D.prototype.minStrokeWidth = 1;

/**
 * Variable: refCount
 *
 * Local counter for references in SVG export.
 */
mxSvgCanvas2D.prototype.refCount = 0;

/**
 * Variable: blockImagePointerEvents
 *
 * Specifies if a transparent rectangle should be added on top of images to absorb
 * all pointer events. Default is false. This is only needed in Firefox to disable
 * control-clicks on images.
 */
mxSvgCanvas2D.prototype.blockImagePointerEvents = false;

/**
 * Variable: lineHeightCorrection
 *
 * Correction factor for <mxConstants.LINE_HEIGHT> in HTML output. Default is 1.
 */
mxSvgCanvas2D.prototype.lineHeightCorrection = 1;

/**
 * Variable: pointerEventsValue
 *
 * Default value for active pointer events. Default is all.
 */
mxSvgCanvas2D.prototype.pointerEventsValue = "all";

/**
 * Variable: fontMetricsPadding
 *
 * Padding to be added for text that is not wrapped to account for differences
 * in font metrics on different platforms in pixels. Default is 10.
 */
mxSvgCanvas2D.prototype.fontMetricsPadding = 10;

/**
 * Variable: cacheOffsetSize
 *
 * Specifies if offsetWidth and offsetHeight should be cached. Default is true.
 * This is used to speed up repaint of text in <updateText>.
 */
mxSvgCanvas2D.prototype.cacheOffsetSize = true;

/**
 * Function: format
 *
 * Rounds all numbers to 2 decimal points.
 */
mxSvgCanvas2D.prototype.format = function (value) {
  return parseFloat(parseFloat(value).toFixed(2));
};

/**
 * Function: getBaseUrl
 *
 * Returns the URL of the page without the hash part. This needs to use href to
 * include any search part with no params (ie question mark alone). This is a
 * workaround for the fact that window.location.search is empty if there is
 * no search string behind the question mark.
 */
mxSvgCanvas2D.prototype.getBaseUrl = function () {
  var href = window.location.href;
  var hash = href.lastIndexOf("#");

  if (hash > 0) {
    href = href.substring(0, hash);
  }

  return href;
};

/**
 * Function: reset
 *
 * Returns any offsets for rendering pixels.
 */
mxSvgCanvas2D.prototype.reset = function () {
  mxAbstractCanvas2D.prototype.reset.apply(this, arguments);
  this.gradients = [];
};

/**
 * Function: createStyle
 *
 * Creates the optional style section.
 */
mxSvgCanvas2D.prototype.createStyle = function (x) {
  var style = this.createElement("style");
  style.setAttribute("type", "text/css");
  mxUtils.write(
    style,
    "svg{font-family:" +
      mxConstants.DEFAULT_FONTFAMILY +
      ";font-size:" +
      mxConstants.DEFAULT_FONTSIZE +
      ";fill:none;stroke-miterlimit:10}"
  );

  return style;
};

/**
 * Function: createElement
 *
 * Private helper function to create SVG elements
 */
mxSvgCanvas2D.prototype.createElement = function (tagName, namespace) {
  if (this.root.ownerDocument.createElementNS != null) {
    return this.root.ownerDocument.createElementNS(
      namespace || mxConstants.NS_SVG,
      tagName
    );
  } else {
    var elt = this.root.ownerDocument.createElement(tagName);

    if (namespace != null) {
      elt.setAttribute("xmlns", namespace);
    }

    return elt;
  }
};

/**
 * Function: getAlternateContent
 *
 * Returns the alternate content for the given foreignObject.
 */
mxSvgCanvas2D.prototype.createAlternateContent = function (
  fo,
  x,
  y,
  w,
  h,
  str,
  align,
  valign,
  wrap,
  format,
  overflow,
  clip,
  rotation
) {
  if (this.foAltText != null) {
    var s = this.state;
    var alt = this.createElement("text");
    alt.setAttribute("x", Math.round(w / 2));
    alt.setAttribute("y", Math.round((h + s.fontSize) / 2));
    alt.setAttribute("fill", s.fontColor || "black");
    alt.setAttribute("text-anchor", "middle");
    alt.setAttribute("font-size", s.fontSize + "px");
    alt.setAttribute("font-family", s.fontFamily);

    if ((s.fontStyle & mxConstants.FONT_BOLD) == mxConstants.FONT_BOLD) {
      alt.setAttribute("font-weight", "bold");
    }

    if ((s.fontStyle & mxConstants.FONT_ITALIC) == mxConstants.FONT_ITALIC) {
      alt.setAttribute("font-style", "italic");
    }

    if (
      (s.fontStyle & mxConstants.FONT_UNDERLINE) ==
      mxConstants.FONT_UNDERLINE
    ) {
      alt.setAttribute("text-decoration", "underline");
    }

    mxUtils.write(alt, this.foAltText);

    return alt;
  } else {
    return null;
  }
};

/**
 * Function: createGradientId
 *
 * Private helper function to create SVG elements
 */
mxSvgCanvas2D.prototype.createGradientId = function (
  start,
  end,
  alpha1,
  alpha2,
  direction
) {
  // Removes illegal characters from gradient ID
  if (start.charAt(0) == "#") {
    start = start.substring(1);
  }

  if (end.charAt(0) == "#") {
    end = end.substring(1);
  }

  // Workaround for gradient IDs not working in Safari 5 / Chrome 6
  // if they contain uppercase characters
  start = start.toLowerCase() + "-" + alpha1;
  end = end.toLowerCase() + "-" + alpha2;

  // Wrong gradient directions possible?
  var dir = null;

  if (direction == null || direction == mxConstants.DIRECTION_SOUTH) {
    dir = "s";
  } else if (direction == mxConstants.DIRECTION_EAST) {
    dir = "e";
  } else {
    var tmp = start;
    start = end;
    end = tmp;

    if (direction == mxConstants.DIRECTION_NORTH) {
      dir = "s";
    } else if (direction == mxConstants.DIRECTION_WEST) {
      dir = "e";
    }
  }

  return "mx-gradient-" + start + "-" + end + "-" + dir;
};

/**
 * Function: getSvgGradient
 *
 * Private helper function to create SVG elements
 */
mxSvgCanvas2D.prototype.getSvgGradient = function (
  start,
  end,
  alpha1,
  alpha2,
  direction
) {
  var id = this.createGradientId(start, end, alpha1, alpha2, direction);
  var gradient = this.gradients[id];

  if (gradient == null) {
    var svg = this.root.ownerSVGElement;

    var counter = 0;
    var tmpId = id + "-" + counter;

    if (svg != null) {
      gradient = svg.ownerDocument.getElementById(tmpId);

      while (gradient != null && gradient.ownerSVGElement != svg) {
        tmpId = id + "-" + counter++;
        gradient = svg.ownerDocument.getElementById(tmpId);
      }
    } else {
      // Uses shorter IDs for export
      tmpId = "id" + ++this.refCount;
    }

    if (gradient == null) {
      gradient = this.createSvgGradient(start, end, alpha1, alpha2, direction);
      gradient.setAttribute("id", tmpId);

      if (this.defs != null) {
        this.defs.appendChild(gradient);
      } else {
        svg.appendChild(gradient);
      }
    }

    this.gradients[id] = gradient;
  }

  return gradient.getAttribute("id");
};

/**
 * Function: createSvgGradient
 *
 * Creates the given SVG gradient.
 */
mxSvgCanvas2D.prototype.createSvgGradient = function (
  start,
  end,
  alpha1,
  alpha2,
  direction
) {
  var gradient = this.createElement("linearGradient");
  gradient.setAttribute("x1", "0%");
  gradient.setAttribute("y1", "0%");
  gradient.setAttribute("x2", "0%");
  gradient.setAttribute("y2", "0%");

  if (direction == null || direction == mxConstants.DIRECTION_SOUTH) {
    gradient.setAttribute("y2", "100%");
  } else if (direction == mxConstants.DIRECTION_EAST) {
    gradient.setAttribute("x2", "100%");
  } else if (direction == mxConstants.DIRECTION_NORTH) {
    gradient.setAttribute("y1", "100%");
  } else if (direction == mxConstants.DIRECTION_WEST) {
    gradient.setAttribute("x1", "100%");
  }

  var op = alpha1 < 1 ? ";stop-opacity:" + alpha1 : "";

  var stop = this.createElement("stop");
  stop.setAttribute("offset", "0%");
  stop.setAttribute("style", "stop-color:" + start + op);
  gradient.appendChild(stop);

  op = alpha2 < 1 ? ";stop-opacity:" + alpha2 : "";

  stop = this.createElement("stop");
  stop.setAttribute("offset", "100%");
  stop.setAttribute("style", "stop-color:" + end + op);
  gradient.appendChild(stop);

  return gradient;
};

/**
 * Function: addNode
 *
 * Private helper function to create SVG elements
 */
mxSvgCanvas2D.prototype.addNode = function (filled, stroked) {
  var node = this.node;
  var s = this.state;

  if (node != null) {
    if (node.nodeName == "path") {
      // Checks if the path is not empty
      if (this.path != null && this.path.length > 0) {
        node.setAttribute("d", this.path.join(" "));
      } else {
        return;
      }
    }

    if (filled && s.fillColor != null) {
      this.updateFill();
    } else if (!this.styleEnabled) {
      // Workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=814952
      if (node.nodeName == "ellipse" && mxClient.IS_FF) {
        node.setAttribute("fill", "transparent");
      } else {
        node.setAttribute("fill", "none");
      }

      // Sets the actual filled state for stroke tolerance
      filled = false;
    }

    if (stroked && s.strokeColor != null) {
      this.updateStroke();
    } else if (!this.styleEnabled) {
      node.setAttribute("stroke", "none");
    }

    if (s.transform != null && s.transform.length > 0) {
      node.setAttribute("transform", s.transform);
    }

    if (s.shadow) {
      this.root.appendChild(this.createShadow(node));
    }

    // Adds stroke tolerance
    if (this.strokeTolerance > 0 && !filled) {
      this.root.appendChild(this.createTolerance(node));
    }

    // Adds pointer events
    if (this.pointerEvents) {
      node.setAttribute("pointer-events", this.pointerEventsValue);
    }
    // Enables clicks for nodes inside a link element
    else if (!this.pointerEvents && this.originalRoot == null) {
      node.setAttribute("pointer-events", "none");
    }

    // Removes invisible nodes from output if they don't handle events
    if (
      (node.nodeName != "rect" &&
        node.nodeName != "path" &&
        node.nodeName != "ellipse") ||
      (node.getAttribute("fill") != "none" &&
        node.getAttribute("fill") != "transparent") ||
      node.getAttribute("stroke") != "none" ||
      node.getAttribute("pointer-events") != "none"
    ) {
      // LATER: Update existing DOM for performance
      this.root.appendChild(node);
    }

    this.node = null;
  }
};

/**
 * Function: updateFill
 *
 * Transfers the stroke attributes from <state> to <node>.
 */
mxSvgCanvas2D.prototype.updateFill = function () {
  var s = this.state;

  if (s.alpha < 1 || s.fillAlpha < 1) {
    this.node.setAttribute("fill-opacity", s.alpha * s.fillAlpha);
  }

  if (s.fillColor != null) {
    if (s.gradientColor != null) {
      var id = this.getSvgGradient(
        String(s.fillColor),
        String(s.gradientColor),
        s.gradientFillAlpha,
        s.gradientAlpha,
        s.gradientDirection
      );

      if (
        !mxClient.IS_CHROMEAPP &&
        !mxClient.IS_IE &&
        !mxClient.IS_IE11 &&
        !mxClient.IS_EDGE &&
        this.root.ownerDocument == document
      ) {
        // Workaround for potential base tag and brackets must be escaped
        var base = this.getBaseUrl().replace(/([\(\)])/g, "\\$1");
        this.node.setAttribute("fill", "url(" + base + "#" + id + ")");
      } else {
        this.node.setAttribute("fill", "url(#" + id + ")");
      }
    } else {
      this.node.setAttribute("fill", String(s.fillColor).toLowerCase());
    }
  }
};

/**
 * Function: getCurrentStrokeWidth
 *
 * Returns the current stroke width (>= 1), ie. max(1, this.format(this.state.strokeWidth * this.state.scale)).
 */
mxSvgCanvas2D.prototype.getCurrentStrokeWidth = function () {
  return Math.max(
    this.minStrokeWidth,
    Math.max(0.01, this.format(this.state.strokeWidth * this.state.scale))
  );
};

/**
 * Function: updateStroke
 *
 * Transfers the stroke attributes from <state> to <node>.
 */
mxSvgCanvas2D.prototype.updateStroke = function () {
  var s = this.state;

  this.node.setAttribute("stroke", String(s.strokeColor).toLowerCase());

  if (s.alpha < 1 || s.strokeAlpha < 1) {
    this.node.setAttribute("stroke-opacity", s.alpha * s.strokeAlpha);
  }

  var sw = this.getCurrentStrokeWidth();

  if (sw != 1) {
    this.node.setAttribute("stroke-width", sw);
  }

  if (this.node.nodeName == "path") {
    this.updateStrokeAttributes();
  }

  if (s.dashed) {
    this.node.setAttribute(
      "stroke-dasharray",
      this.createDashPattern((s.fixDash ? 1 : s.strokeWidth) * s.scale)
    );
  }
};

/**
 * Function: updateStrokeAttributes
 *
 * Transfers the stroke attributes from <state> to <node>.
 */
mxSvgCanvas2D.prototype.updateStrokeAttributes = function () {
  var s = this.state;

  // Linejoin miter is default in SVG
  if (s.lineJoin != null && s.lineJoin != "miter") {
    this.node.setAttribute("stroke-linejoin", s.lineJoin);
  }

  if (s.lineCap != null) {
    // flat is called butt in SVG
    var value = s.lineCap;

    if (value == "flat") {
      value = "butt";
    }

    // Linecap butt is default in SVG
    if (value != "butt") {
      this.node.setAttribute("stroke-linecap", value);
    }
  }

  // Miterlimit 10 is default in our document
  if (s.miterLimit != null && (!this.styleEnabled || s.miterLimit != 10)) {
    this.node.setAttribute("stroke-miterlimit", s.miterLimit);
  }
};

/**
 * Function: createDashPattern
 *
 * Creates the SVG dash pattern for the given state.
 */
mxSvgCanvas2D.prototype.createDashPattern = function (scale) {
  var pat = [];

  if (typeof this.state.dashPattern === "string") {
    var dash = this.state.dashPattern.split(" ");

    if (dash.length > 0) {
      for (var i = 0; i < dash.length; i++) {
        pat[i] = Number(dash[i]) * scale;
      }
    }
  }

  return pat.join(" ");
};

/**
 * Function: createTolerance
 *
 * Creates a hit detection tolerance shape for the given node.
 */
mxSvgCanvas2D.prototype.createTolerance = function (node) {
  var tol = node.cloneNode(true);
  var sw =
    parseFloat(tol.getAttribute("stroke-width") || 1) + this.strokeTolerance;
  tol.setAttribute("pointer-events", "stroke");
  tol.setAttribute("visibility", "hidden");
  tol.removeAttribute("stroke-dasharray");
  tol.setAttribute("stroke-width", sw);
  tol.setAttribute("fill", "none");

  // Workaround for Opera ignoring the visiblity attribute above while
  // other browsers need a stroke color to perform the hit-detection but
  // do not ignore the visibility attribute. Side-effect is that Opera's
  // hit detection for horizontal/vertical edges seems to ignore the tol.
  tol.setAttribute("stroke", mxClient.IS_OT ? "none" : "white");

  return tol;
};

/**
 * Function: createShadow
 *
 * Creates a shadow for the given node.
 */
mxSvgCanvas2D.prototype.createShadow = function (node) {
  var shadow = node.cloneNode(true);
  var s = this.state;

  // Firefox uses transparent for no fill in ellipses
  if (
    shadow.getAttribute("fill") != "none" &&
    (!mxClient.IS_FF || shadow.getAttribute("fill") != "transparent")
  ) {
    shadow.setAttribute("fill", s.shadowColor);
  }

  if (shadow.getAttribute("stroke") != "none") {
    shadow.setAttribute("stroke", s.shadowColor);
  }

  shadow.setAttribute(
    "transform",
    "translate(" +
      this.format(s.shadowDx * s.scale) +
      "," +
      this.format(s.shadowDy * s.scale) +
      ")" +
      (s.transform || "")
  );
  shadow.setAttribute("opacity", s.shadowAlpha);

  return shadow;
};

/**
 * Function: setLink
 *
 * Experimental implementation for hyperlinks.
 */
mxSvgCanvas2D.prototype.setLink = function (link) {
  if (link == null) {
    this.root = this.originalRoot;
  } else {
    this.originalRoot = this.root;

    var node = this.createElement("a");

    // Workaround for implicit namespace handling in HTML5 export, IE adds NS1 namespace so use code below
    // in all IE versions except quirks mode. KNOWN: Adds xlink namespace to each image tag in output.
    if (
      node.setAttributeNS == null ||
      (this.root.ownerDocument != document && document.documentMode == null)
    ) {
      node.setAttribute("xlink:href", link);
    } else {
      node.setAttributeNS(mxConstants.NS_XLINK, "xlink:href", link);
    }

    this.root.appendChild(node);
    this.root = node;
  }
};

/**
 * Function: rotate
 *
 * Sets the rotation of the canvas. Note that rotation cannot be concatenated.
 */
mxSvgCanvas2D.prototype.rotate = function (theta, flipH, flipV, cx, cy) {
  if (theta != 0 || flipH || flipV) {
    var s = this.state;
    cx += s.dx;
    cy += s.dy;

    cx *= s.scale;
    cy *= s.scale;

    s.transform = s.transform || "";

    // This implementation uses custom scale/translate and built-in rotation
    // Rotation state is part of the AffineTransform in state.transform
    if (flipH && flipV) {
      theta += 180;
    } else if (flipH != flipV) {
      var tx = flipH ? cx : 0;
      var sx = flipH ? -1 : 1;

      var ty = flipV ? cy : 0;
      var sy = flipV ? -1 : 1;

      s.transform +=
        "translate(" +
        this.format(tx) +
        "," +
        this.format(ty) +
        ")" +
        "scale(" +
        this.format(sx) +
        "," +
        this.format(sy) +
        ")" +
        "translate(" +
        this.format(-tx) +
        "," +
        this.format(-ty) +
        ")";
    }

    if (flipH ? !flipV : flipV) {
      theta *= -1;
    }

    if (theta != 0) {
      s.transform +=
        "rotate(" +
        this.format(theta) +
        "," +
        this.format(cx) +
        "," +
        this.format(cy) +
        ")";
    }

    s.rotation = s.rotation + theta;
    s.rotationCx = cx;
    s.rotationCy = cy;
  }
};

/**
 * Function: begin
 *
 * Extends superclass to create path.
 */
mxSvgCanvas2D.prototype.begin = function () {
  mxAbstractCanvas2D.prototype.begin.apply(this, arguments);
  this.node = this.createElement("path");
};

/**
 * Function: rect
 *
 * Private helper function to create SVG elements
 */
mxSvgCanvas2D.prototype.rect = function (x, y, w, h) {
  var s = this.state;
  var n = this.createElement("rect");
  n.setAttribute("x", this.format((x + s.dx) * s.scale));
  n.setAttribute("y", this.format((y + s.dy) * s.scale));
  n.setAttribute("width", this.format(w * s.scale));
  n.setAttribute("height", this.format(h * s.scale));

  this.node = n;
};

/**
 * Function: roundrect
 *
 * Private helper function to create SVG elements
 */
mxSvgCanvas2D.prototype.roundrect = function (x, y, w, h, dx, dy) {
  this.rect(x, y, w, h);

  if (dx > 0) {
    this.node.setAttribute("rx", this.format(dx * this.state.scale));
  }

  if (dy > 0) {
    this.node.setAttribute("ry", this.format(dy * this.state.scale));
  }
};

/**
 * Function: ellipse
 *
 * Private helper function to create SVG elements
 */
mxSvgCanvas2D.prototype.ellipse = function (x, y, w, h) {
  var s = this.state;
  var n = this.createElement("ellipse");
  // No rounding for consistent output with 1.x
  n.setAttribute("cx", this.format((x + w / 2 + s.dx) * s.scale));
  n.setAttribute("cy", this.format((y + h / 2 + s.dy) * s.scale));
  n.setAttribute("rx", (w / 2) * s.scale);
  n.setAttribute("ry", (h / 2) * s.scale);
  this.node = n;
};

/**
 * Function: image
 *
 * Private helper function to create SVG elements
 */
mxSvgCanvas2D.prototype.image = function (
  x,
  y,
  w,
  h,
  src,
  aspect,
  flipH,
  flipV
) {
  src = this.converter.convert(src);

  // LATER: Add option for embedding images as base64.
  aspect = aspect != null ? aspect : true;
  flipH = flipH != null ? flipH : false;
  flipV = flipV != null ? flipV : false;

  var s = this.state;
  x += s.dx;
  y += s.dy;

  var node = this.createElement("image");
  node.setAttribute("x", this.format(x * s.scale) + this.imageOffset);
  node.setAttribute("y", this.format(y * s.scale) + this.imageOffset);
  node.setAttribute("width", this.format(w * s.scale));
  node.setAttribute("height", this.format(h * s.scale));

  // Workaround for missing namespace support
  if (node.setAttributeNS == null) {
    node.setAttribute("xlink:href", src);
  } else {
    node.setAttributeNS(mxConstants.NS_XLINK, "xlink:href", src);
  }

  if (!aspect) {
    node.setAttribute("preserveAspectRatio", "none");
  }

  if (s.alpha < 1 || s.fillAlpha < 1) {
    node.setAttribute("opacity", s.alpha * s.fillAlpha);
  }

  var tr = this.state.transform || "";

  if (flipH || flipV) {
    var sx = 1;
    var sy = 1;
    var dx = 0;
    var dy = 0;

    if (flipH) {
      sx = -1;
      dx = -w - 2 * x;
    }

    if (flipV) {
      sy = -1;
      dy = -h - 2 * y;
    }

    // Adds image tansformation to existing transform
    tr +=
      "scale(" +
      sx +
      "," +
      sy +
      ")translate(" +
      dx * s.scale +
      "," +
      dy * s.scale +
      ")";
  }

  if (tr.length > 0) {
    node.setAttribute("transform", tr);
  }

  if (!this.pointerEvents) {
    node.setAttribute("pointer-events", "none");
  }

  this.root.appendChild(node);

  // Disables control-clicks on images in Firefox to open in new tab
  // by putting a rect in the foreground that absorbs all events and
  // disabling all pointer-events on the original image tag.
  if (this.blockImagePointerEvents) {
    node.setAttribute("style", "pointer-events:none");

    node = this.createElement("rect");
    node.setAttribute("visibility", "hidden");
    node.setAttribute("pointer-events", "fill");
    node.setAttribute("x", this.format(x * s.scale));
    node.setAttribute("y", this.format(y * s.scale));
    node.setAttribute("width", this.format(w * s.scale));
    node.setAttribute("height", this.format(h * s.scale));
    this.root.appendChild(node);
  }
};

/**
 * Function: convertHtml
 *
 * Converts the given HTML string to XHTML.
 */
mxSvgCanvas2D.prototype.convertHtml = function (val) {
  if (this.useDomParser) {
    var doc = new DOMParser().parseFromString(val, "text/html");

    if (doc != null) {
      val = new XMLSerializer().serializeToString(doc.body);

      // Extracts body content from DOM
      if (val.substring(0, 5) == "<body") {
        val = val.substring(val.indexOf(">", 5) + 1);
      }

      if (val.substring(val.length - 7, val.length) == "</body>") {
        val = val.substring(0, val.length - 7);
      }
    }
  } else if (
    document.implementation != null &&
    document.implementation.createDocument != null
  ) {
    var xd = document.implementation.createDocument(
      "http://www.w3.org/1999/xhtml",
      "html",
      null
    );
    var xb = xd.createElement("body");
    xd.documentElement.appendChild(xb);

    var div = document.createElement("div");
    div.innerHTML = val;
    var child = div.firstChild;

    while (child != null) {
      var next = child.nextSibling;
      xb.appendChild(xd.adoptNode(child));
      child = next;
    }

    return xb.innerHTML;
  } else {
    var ta = document.createElement("textarea");

    // Handles special HTML entities < and > and double escaping
    // and converts unclosed br, hr and img tags to XHTML
    // LATER: Convert all unclosed tags
    ta.innerHTML = val
      .replace(/&amp;/g, "&amp;amp;")
      .replace(/&#60;/g, "&amp;lt;")
      .replace(/&#62;/g, "&amp;gt;")
      .replace(/&lt;/g, "&amp;lt;")
      .replace(/&gt;/g, "&amp;gt;")
      .replace(/</g, "&lt;")
      .replace(/>/g, "&gt;");
    val = ta.value
      .replace(/&/g, "&amp;")
      .replace(/&amp;lt;/g, "&lt;")
      .replace(/&amp;gt;/g, "&gt;")
      .replace(/&amp;amp;/g, "&amp;")
      .replace(/<br>/g, "<br />")
      .replace(/<hr>/g, "<hr />")
      .replace(/(<img[^>]+)>/gm, "$1 />");
  }

  return val;
};

/**
 * Function: createDiv
 *
 * Private helper function to create SVG elements
 */
mxSvgCanvas2D.prototype.createDiv = function (
  str,
  align,
  valign,
  style,
  overflow,
  whiteSpace
) {
  var s = this.state;

  // Inline block for rendering HTML background over SVG in Safari
  var lh = mxConstants.ABSOLUTE_LINE_HEIGHT
    ? s.fontSize * mxConstants.LINE_HEIGHT + "px"
    : mxConstants.LINE_HEIGHT * this.lineHeightCorrection;

  style =
    "display:inline-block;font-size:" +
    s.fontSize +
    "px;font-family:" +
    s.fontFamily +
    ";color:" +
    s.fontColor +
    ";line-height:" +
    lh +
    ";" +
    style;

  if ((s.fontStyle & mxConstants.FONT_BOLD) == mxConstants.FONT_BOLD) {
    style += "font-weight:bold;";
  }

  if ((s.fontStyle & mxConstants.FONT_ITALIC) == mxConstants.FONT_ITALIC) {
    style += "font-style:italic;";
  }

  if (
    (s.fontStyle & mxConstants.FONT_UNDERLINE) ==
    mxConstants.FONT_UNDERLINE
  ) {
    style += "text-decoration:underline;";
  }

  if (align == mxConstants.ALIGN_CENTER) {
    style += "text-align:center;";
  } else if (align == mxConstants.ALIGN_RIGHT) {
    style += "text-align:right;";
  } else {
    style += "text-align:left;";
  }

  var css = "";

  if (s.fontBackgroundColor != null) {
    css +=
      "background-color:" + mxUtils.htmlEntities(s.fontBackgroundColor) + ";";
  }

  if (s.fontBorderColor != null) {
    css += "border:1px solid " + mxUtils.htmlEntities(s.fontBorderColor) + ";";
  }

  var val = str;

  if (!mxUtils.isNode(val)) {
    val = this.convertHtml(val);

    if (overflow != "fill" && overflow != "width") {
      // Workaround for no wrapping in HTML canvas for image
      // export if the inner HTML contains a DIV with width
      if (whiteSpace != null) {
        css += "white-space:" + whiteSpace + ";";
      }

      // Inner div always needed to measure wrapped text
      val =
        '<div xmlns="http://www.w3.org/1999/xhtml" style="display:inline-block;text-align:inherit;text-decoration:inherit;' +
        css +
        '">' +
        val +
        "</div>";
    } else {
      style += css;
    }
  }

  // Uses DOM API where available. This cannot be used in IE to avoid
  // an opening and two (!) closing TBODY tags being added to tables.
  if (!mxClient.IS_IE && document.createElementNS) {
    var div = document.createElementNS("http://www.w3.org/1999/xhtml", "div");
    div.setAttribute("style", style);

    if (mxUtils.isNode(val)) {
      // Creates a copy for export
      if (this.root.ownerDocument != document) {
        div.appendChild(val.cloneNode(true));
      } else {
        div.appendChild(val);
      }
    } else {
      div.innerHTML = val;
    }

    return div;
  } else {
    // Serializes for export
    if (mxUtils.isNode(val) && this.root.ownerDocument != document) {
      val = val.outerHTML;
    }

    // NOTE: FF 3.6 crashes if content CSS contains "height:100%"
    return mxUtils.parseXml(
      '<div xmlns="http://www.w3.org/1999/xhtml" style="' +
        style +
        '">' +
        val +
        "</div>"
    ).documentElement;
  }
};

/**
 * Invalidates the cached offset size for the given node.
 */
mxSvgCanvas2D.prototype.invalidateCachedOffsetSize = function (node) {
  delete node.firstChild.mxCachedOffsetWidth;
  delete node.firstChild.mxCachedFinalOffsetWidth;
  delete node.firstChild.mxCachedFinalOffsetHeight;
};

/**
 * Updates existing DOM nodes for text rendering. LATER: Merge common parts with text function below.
 */
mxSvgCanvas2D.prototype.updateText = function (
  x,
  y,
  w,
  h,
  align,
  valign,
  wrap,
  overflow,
  clip,
  rotation,
  node
) {
  if (
    node != null &&
    node.firstChild != null &&
    node.firstChild.firstChild != null &&
    node.firstChild.firstChild.firstChild != null
  ) {
    // Uses outer group for opacity and transforms to
    // fix rendering order in Chrome
    var group = node.firstChild;
    var fo = group.firstChild;
    var div = fo.firstChild;

    rotation = rotation != null ? rotation : 0;

    var s = this.state;
    x += s.dx;
    y += s.dy;

    if (clip) {
      div.style.maxHeight = Math.round(h) + "px";
      div.style.maxWidth = Math.round(w) + "px";
    } else if (overflow == "fill") {
      div.style.width = Math.round(w + 1) + "px";
      div.style.height = Math.round(h + 1) + "px";
    } else if (overflow == "width") {
      div.style.width = Math.round(w + 1) + "px";

      if (h > 0) {
        div.style.maxHeight = Math.round(h) + "px";
      }
    }

    if (wrap && w > 0) {
      div.style.width = Math.round(w + 1) + "px";
    }

    // Code that depends on the size which is computed after
    // the element was added to the DOM.
    var ow = 0;
    var oh = 0;

    // Padding avoids clipping on border and wrapping for differing font metrics on platforms
    var padX = 0;
    var padY = 2;

    var sizeDiv = div;

    if (sizeDiv.firstChild != null && sizeDiv.firstChild.nodeName == "DIV") {
      sizeDiv = sizeDiv.firstChild;
    }

    var tmp =
      group.mxCachedOffsetWidth != null
        ? group.mxCachedOffsetWidth
        : sizeDiv.offsetWidth;
    ow = tmp + padX;

    // Recomputes the height of the element for wrapped width
    if (wrap && overflow != "fill") {
      if (clip) {
        ow = Math.min(ow, w);
      }

      div.style.width = Math.round(ow + 1) + "px";
    }

    ow =
      group.mxCachedFinalOffsetWidth != null
        ? group.mxCachedFinalOffsetWidth
        : sizeDiv.offsetWidth;
    oh =
      group.mxCachedFinalOffsetHeight != null
        ? group.mxCachedFinalOffsetHeight
        : sizeDiv.offsetHeight;

    if (this.cacheOffsetSize) {
      group.mxCachedOffsetWidth = tmp;
      group.mxCachedFinalOffsetWidth = ow;
      group.mxCachedFinalOffsetHeight = oh;
    }

    ow += padX;
    oh -= 2;

    if (clip) {
      oh = Math.min(oh, h);
      ow = Math.min(ow, w);
    }

    if (overflow == "width") {
      h = oh;
    } else if (overflow != "fill") {
      w = ow;
      h = oh;
    }

    var dx = 0;
    var dy = 0;

    if (align == mxConstants.ALIGN_CENTER) {
      dx -= w / 2;
    } else if (align == mxConstants.ALIGN_RIGHT) {
      dx -= w;
    }

    x += dx;

    // FIXME: LINE_HEIGHT not ideal for all text sizes, fix for export
    if (valign == mxConstants.ALIGN_MIDDLE) {
      dy -= h / 2;
    } else if (valign == mxConstants.ALIGN_BOTTOM) {
      dy -= h;
    }

    // Workaround for rendering offsets
    // TODO: Check if export needs these fixes, too
    if (overflow != "fill" && mxClient.IS_FF && mxClient.IS_WIN) {
      dy -= 2;
    }

    y += dy;

    var tr = s.scale != 1 ? "scale(" + s.scale + ")" : "";

    if (s.rotation != 0 && this.rotateHtml) {
      tr += "rotate(" + s.rotation + "," + w / 2 + "," + h / 2 + ")";
      var pt = this.rotatePoint(
        (x + w / 2) * s.scale,
        (y + h / 2) * s.scale,
        s.rotation,
        s.rotationCx,
        s.rotationCy
      );
      x = pt.x - (w * s.scale) / 2;
      y = pt.y - (h * s.scale) / 2;
    } else {
      x *= s.scale;
      y *= s.scale;
    }

    if (rotation != 0) {
      tr += "rotate(" + rotation + "," + -dx + "," + -dy + ")";
    }

    group.setAttribute(
      "transform",
      "translate(" + Math.round(x) + "," + Math.round(y) + ")" + tr
    );
    fo.setAttribute("width", Math.round(Math.max(1, w)));
    fo.setAttribute("height", Math.round(Math.max(1, h)));
  }
};

/**
 * Function: text
 *
 * Paints the given text. Possible values for format are empty string for plain
 * text and html for HTML markup. Note that HTML markup is only supported if
 * foreignObject is supported and <foEnabled> is true. (This means IE9 and later
 * does currently not support HTML text as part of shapes.)
 */
mxSvgCanvas2D.prototype.text = function (
  x,
  y,
  w,
  h,
  str,
  align,
  valign,
  wrap,
  format,
  overflow,
  clip,
  rotation,
  dir
) {
  if (this.textEnabled && str != null) {
    rotation = rotation != null ? rotation : 0;

    var s = this.state;
    x += s.dx;
    y += s.dy;

    if (this.foEnabled && format == "html") {
      var style = "vertical-align:top;";

      if (clip) {
        style +=
          "overflow:hidden;max-height:" +
          Math.round(h) +
          "px;max-width:" +
          Math.round(w) +
          "px;";
      } else if (overflow == "fill") {
        style +=
          "width:" +
          Math.round(w + 1) +
          "px;height:" +
          Math.round(h + 1) +
          "px;overflow:hidden;";
      } else if (overflow == "width") {
        style += "width:" + Math.round(w + 1) + "px;";

        if (h > 0) {
          style += "max-height:" + Math.round(h) + "px;overflow:hidden;";
        }
      }

      if (wrap && w > 0) {
        style +=
          "width:" +
          Math.round(w + 1) +
          "px;white-space:normal;word-wrap:" +
          mxConstants.WORD_WRAP +
          ";";
      } else {
        style += "white-space:nowrap;";
      }

      // Uses outer group for opacity and transforms to
      // fix rendering order in Chrome
      var group = this.createElement("g");

      if (s.alpha < 1) {
        group.setAttribute("opacity", s.alpha);
      }

      var fo = this.createElement("foreignObject");
      fo.setAttribute("style", "overflow:visible;");
      fo.setAttribute(
        "pointer-events",
        this.pointerEvents ? this.pointerEventsValue : "none"
      );

      var div = this.createDiv(
        str,
        align,
        valign,
        style,
        overflow,
        wrap && w > 0 ? "normal" : null
      );

      // Ignores invalid XHTML labels
      if (div == null) {
        return;
      } else if (dir != null) {
        div.setAttribute("dir", dir);
      }

      group.appendChild(fo);
      this.root.appendChild(group);

      // Code that depends on the size which is computed after
      // the element was added to the DOM.
      var ow = 0;
      var oh = 0;

      // Padding avoids clipping on border and wrapping for differing font metrics on platforms
      var padX = 2;
      var padY = 2;

      // NOTE: IE is always export as it does not support foreign objects
      if (mxClient.IS_IE && (document.documentMode == 9 || !mxClient.IS_SVG)) {
        // Handles non-standard namespace for getting size in IE
        var clone = document.createElement("div");

        clone.style.cssText = div.getAttribute("style");
        clone.style.display = mxClient.IS_QUIRKS ? "inline" : "inline-block";
        clone.style.position = "absolute";
        clone.style.visibility = "hidden";

        // Inner DIV is needed for text measuring
        var div2 = document.createElement("div");
        div2.style.display = mxClient.IS_QUIRKS ? "inline" : "inline-block";
        div2.style.wordWrap = mxConstants.WORD_WRAP;
        div2.innerHTML = mxUtils.isNode(str) ? str.outerHTML : str;
        clone.appendChild(div2);

        document.body.appendChild(clone);

        // Workaround for different box models
        if (
          document.documentMode != 8 &&
          document.documentMode != 9 &&
          s.fontBorderColor != null
        ) {
          padX += 2;
          padY += 2;
        }

        if (wrap && w > 0) {
          var tmp = div2.offsetWidth;

          // Workaround for adding padding twice in IE8/IE9 standards mode if label is wrapped
          padDx = 0;

          // For export, if no wrapping occurs, we add a large padding to make
          // sure there is no wrapping even if the text metrics are different.
          // This adds support for text metrics on different operating systems.
          // Disables wrapping if text is not wrapped for given width
          if (
            !clip &&
            wrap &&
            w > 0 &&
            this.root.ownerDocument != document &&
            overflow != "fill"
          ) {
            var ws = clone.style.whiteSpace;
            div2.style.whiteSpace = "nowrap";

            if (tmp < div2.offsetWidth) {
              clone.style.whiteSpace = ws;
            }
          }

          if (clip) {
            tmp = Math.min(tmp, w);
          }

          clone.style.width = tmp + "px";

          // Padding avoids clipping on border
          ow = div2.offsetWidth + padX + padDx;
          oh = div2.offsetHeight + padY;

          // Overrides the width of the DIV via XML DOM by using the
          // clone DOM style, getting the CSS text for that and
          // then setting that on the DIV via setAttribute
          clone.style.display = "inline-block";
          clone.style.position = "";
          clone.style.visibility = "";
          clone.style.width = ow + "px";

          div.setAttribute("style", clone.style.cssText);
        } else {
          // Padding avoids clipping on border
          ow = div2.offsetWidth + padX;
          oh = div2.offsetHeight + padY;
        }

        clone.parentNode.removeChild(clone);
        fo.appendChild(div);
      } else {
        // Uses document for text measuring during export
        if (this.root.ownerDocument != document) {
          div.style.visibility = "hidden";
          document.body.appendChild(div);
        } else {
          fo.appendChild(div);
        }

        var sizeDiv = div;

        if (
          sizeDiv.firstChild != null &&
          sizeDiv.firstChild.nodeName == "DIV"
        ) {
          sizeDiv = sizeDiv.firstChild;

          if (wrap && div.style.wordWrap == "break-word") {
            sizeDiv.style.width = "100%";
          }
        }

        var tmp = sizeDiv.offsetWidth;

        // Workaround for text measuring in hidden containers
        if (tmp == 0 && div.parentNode == fo) {
          div.style.visibility = "hidden";
          document.body.appendChild(div);

          tmp = sizeDiv.offsetWidth;
        }

        if (this.cacheOffsetSize) {
          group.mxCachedOffsetWidth = tmp;
        }

        // Disables wrapping if text is not wrapped for given width
        if (
          !clip &&
          wrap &&
          w > 0 &&
          this.root.ownerDocument != document &&
          overflow != "fill" &&
          overflow != "width"
        ) {
          var ws = div.style.whiteSpace;
          div.style.whiteSpace = "nowrap";

          if (tmp < sizeDiv.offsetWidth) {
            div.style.whiteSpace = ws;
          }
        }

        ow = tmp + padX - 1;

        // Recomputes the height of the element for wrapped width
        if (wrap && overflow != "fill" && overflow != "width") {
          if (clip) {
            ow = Math.min(ow, w);
          }

          div.style.width = ow + "px";
        }

        ow = sizeDiv.offsetWidth;
        oh = sizeDiv.offsetHeight;

        if (this.cacheOffsetSize) {
          group.mxCachedFinalOffsetWidth = ow;
          group.mxCachedFinalOffsetHeight = oh;
        }

        oh -= padY;

        if (div.parentNode != fo) {
          fo.appendChild(div);
          div.style.visibility = "";
        }
      }

      if (clip) {
        oh = Math.min(oh, h);
        ow = Math.min(ow, w);
      }

      if (overflow == "width") {
        h = oh;
      } else if (overflow != "fill") {
        w = ow;
        h = oh;
      }

      if (s.alpha < 1) {
        group.setAttribute("opacity", s.alpha);
      }

      var dx = 0;
      var dy = 0;

      if (align == mxConstants.ALIGN_CENTER) {
        dx -= w / 2;
      } else if (align == mxConstants.ALIGN_RIGHT) {
        dx -= w;
      }

      x += dx;

      // FIXME: LINE_HEIGHT not ideal for all text sizes, fix for export
      if (valign == mxConstants.ALIGN_MIDDLE) {
        dy -= h / 2;
      } else if (valign == mxConstants.ALIGN_BOTTOM) {
        dy -= h;
      }

      // Workaround for rendering offsets
      // TODO: Check if export needs these fixes, too
      //if (this.root.ownerDocument == document)
      if (overflow != "fill" && mxClient.IS_FF && mxClient.IS_WIN) {
        dy -= 2;
      }

      y += dy;

      var tr = s.scale != 1 ? "scale(" + s.scale + ")" : "";

      if (s.rotation != 0 && this.rotateHtml) {
        tr += "rotate(" + s.rotation + "," + w / 2 + "," + h / 2 + ")";
        var pt = this.rotatePoint(
          (x + w / 2) * s.scale,
          (y + h / 2) * s.scale,
          s.rotation,
          s.rotationCx,
          s.rotationCy
        );
        x = pt.x - (w * s.scale) / 2;
        y = pt.y - (h * s.scale) / 2;
      } else {
        x *= s.scale;
        y *= s.scale;
      }

      if (rotation != 0) {
        tr += "rotate(" + rotation + "," + -dx + "," + -dy + ")";
      }

      group.setAttribute(
        "transform",
        "translate(" +
          (Math.round(x) + this.foOffset) +
          "," +
          (Math.round(y) + this.foOffset) +
          ")" +
          tr
      );
      fo.setAttribute("width", Math.round(Math.max(1, w)));
      fo.setAttribute("height", Math.round(Math.max(1, h)));

      // Adds alternate content if foreignObject not supported in viewer
      if (this.root.ownerDocument != document) {
        var alt = this.createAlternateContent(
          fo,
          x,
          y,
          w,
          h,
          str,
          align,
          valign,
          wrap,
          format,
          overflow,
          clip,
          rotation
        );

        if (alt != null) {
          fo.setAttribute(
            "requiredFeatures",
            "http://www.w3.org/TR/SVG11/feature#Extensibility"
          );
          var sw = this.createElement("switch");
          sw.appendChild(fo);
          sw.appendChild(alt);
          group.appendChild(sw);
        }
      }
    } else {
      this.plainText(
        x,
        y,
        w,
        h,
        str,
        align,
        valign,
        wrap,
        overflow,
        clip,
        rotation,
        dir
      );
    }
  }
};

/**
 * Function: createClip
 *
 * Creates a clip for the given coordinates.
 */
mxSvgCanvas2D.prototype.createClip = function (x, y, w, h) {
  x = Math.round(x);
  y = Math.round(y);
  w = Math.round(w);
  h = Math.round(h);

  var id = "mx-clip-" + x + "-" + y + "-" + w + "-" + h;

  var counter = 0;
  var tmp = id + "-" + counter;

  // Resolves ID conflicts
  while (document.getElementById(tmp) != null) {
    tmp = id + "-" + ++counter;
  }

  clip = this.createElement("clipPath");
  clip.setAttribute("id", tmp);

  var rect = this.createElement("rect");
  rect.setAttribute("x", x);
  rect.setAttribute("y", y);
  rect.setAttribute("width", w);
  rect.setAttribute("height", h);

  clip.appendChild(rect);

  return clip;
};

/**
 * Function: text
 *
 * Paints the given text. Possible values for format are empty string for
 * plain text and html for HTML markup.
 */
mxSvgCanvas2D.prototype.plainText = function (
  x,
  y,
  w,
  h,
  str,
  align,
  valign,
  wrap,
  overflow,
  clip,
  rotation,
  dir
) {
  rotation = rotation != null ? rotation : 0;
  var s = this.state;
  var size = s.fontSize;
  var node = this.createElement("g");
  var tr = s.transform || "";
  this.updateFont(node);

  // Non-rotated text
  if (rotation != 0) {
    tr +=
      "rotate(" +
      rotation +
      "," +
      this.format(x * s.scale) +
      "," +
      this.format(y * s.scale) +
      ")";
  }

  if (dir != null) {
    node.setAttribute("direction", dir);
  }

  if (clip && w > 0 && h > 0) {
    var cx = x;
    var cy = y;

    if (align == mxConstants.ALIGN_CENTER) {
      cx -= w / 2;
    } else if (align == mxConstants.ALIGN_RIGHT) {
      cx -= w;
    }

    if (overflow != "fill") {
      if (valign == mxConstants.ALIGN_MIDDLE) {
        cy -= h / 2;
      } else if (valign == mxConstants.ALIGN_BOTTOM) {
        cy -= h;
      }
    }

    // LATER: Remove spacing from clip rectangle
    var c = this.createClip(
      cx * s.scale - 2,
      cy * s.scale - 2,
      w * s.scale + 4,
      h * s.scale + 4
    );

    if (this.defs != null) {
      this.defs.appendChild(c);
    } else {
      // Makes sure clip is removed with referencing node
      this.root.appendChild(c);
    }

    if (
      !mxClient.IS_CHROMEAPP &&
      !mxClient.IS_IE &&
      !mxClient.IS_IE11 &&
      !mxClient.IS_EDGE &&
      this.root.ownerDocument == document
    ) {
      // Workaround for potential base tag
      var base = this.getBaseUrl().replace(/([\(\)])/g, "\\$1");
      node.setAttribute(
        "clip-path",
        "url(" + base + "#" + c.getAttribute("id") + ")"
      );
    } else {
      node.setAttribute("clip-path", "url(#" + c.getAttribute("id") + ")");
    }
  }

  // Default is left
  var anchor =
    align == mxConstants.ALIGN_RIGHT
      ? "end"
      : align == mxConstants.ALIGN_CENTER
      ? "middle"
      : "start";

  // Text-anchor start is default in SVG
  if (anchor != "start") {
    node.setAttribute("text-anchor", anchor);
  }

  if (!this.styleEnabled || size != mxConstants.DEFAULT_FONTSIZE) {
    node.setAttribute("font-size", size * s.scale + "px");
  }

  if (tr.length > 0) {
    node.setAttribute("transform", tr);
  }

  if (s.alpha < 1) {
    node.setAttribute("opacity", s.alpha);
  }

  var lines = str.split("\n");
  var lh = Math.round(size * mxConstants.LINE_HEIGHT);
  var textHeight = size + (lines.length - 1) * lh;

  var cy = y + size - 1;

  if (valign == mxConstants.ALIGN_MIDDLE) {
    if (overflow == "fill") {
      cy -= h / 2;
    } else {
      var dy =
        (this.matchHtmlAlignment && clip && h > 0
          ? Math.min(textHeight, h)
          : textHeight) / 2;
      cy -= dy + 1;
    }
  } else if (valign == mxConstants.ALIGN_BOTTOM) {
    if (overflow == "fill") {
      cy -= h;
    } else {
      var dy =
        this.matchHtmlAlignment && clip && h > 0
          ? Math.min(textHeight, h)
          : textHeight;
      cy -= dy + 2;
    }
  }

  for (var i = 0; i < lines.length; i++) {
    // Workaround for bounding box of empty lines and spaces
    if (lines[i].length > 0 && mxUtils.trim(lines[i]).length > 0) {
      var text = this.createElement("text");
      // LATER: Match horizontal HTML alignment
      text.setAttribute("x", this.format(x * s.scale) + this.textOffset);
      text.setAttribute("y", this.format(cy * s.scale) + this.textOffset);

      mxUtils.write(text, lines[i]);
      node.appendChild(text);
    }

    cy += lh;
  }

  this.root.appendChild(node);
  this.addTextBackground(
    node,
    str,
    x,
    y,
    w,
    overflow == "fill" ? h : textHeight,
    align,
    valign,
    overflow
  );
};

/**
 * Function: updateFont
 *
 * Updates the text properties for the given node. (NOTE: For this to work in
 * IE, the given node must be a text or tspan element.)
 */
mxSvgCanvas2D.prototype.updateFont = function (node) {
  var s = this.state;

  node.setAttribute("fill", s.fontColor);

  if (!this.styleEnabled || s.fontFamily != mxConstants.DEFAULT_FONTFAMILY) {
    node.setAttribute("font-family", s.fontFamily);
  }

  if ((s.fontStyle & mxConstants.FONT_BOLD) == mxConstants.FONT_BOLD) {
    node.setAttribute("font-weight", "bold");
  }

  if ((s.fontStyle & mxConstants.FONT_ITALIC) == mxConstants.FONT_ITALIC) {
    node.setAttribute("font-style", "italic");
  }

  if (
    (s.fontStyle & mxConstants.FONT_UNDERLINE) ==
    mxConstants.FONT_UNDERLINE
  ) {
    node.setAttribute("text-decoration", "underline");
  }
};

/**
 * Function: addTextBackground
 *
 * Background color and border
 */
mxSvgCanvas2D.prototype.addTextBackground = function (
  node,
  str,
  x,
  y,
  w,
  h,
  align,
  valign,
  overflow
) {
  var s = this.state;

  if (s.fontBackgroundColor != null || s.fontBorderColor != null) {
    var bbox = null;

    if (overflow == "fill" || overflow == "width") {
      if (align == mxConstants.ALIGN_CENTER) {
        x -= w / 2;
      } else if (align == mxConstants.ALIGN_RIGHT) {
        x -= w;
      }

      if (valign == mxConstants.ALIGN_MIDDLE) {
        y -= h / 2;
      } else if (valign == mxConstants.ALIGN_BOTTOM) {
        y -= h;
      }

      bbox = new mxRectangle(
        (x + 1) * s.scale,
        y * s.scale,
        (w - 2) * s.scale,
        (h + 2) * s.scale
      );
    } else if (node.getBBox != null && this.root.ownerDocument == document) {
      // Uses getBBox only if inside document for correct size
      try {
        bbox = node.getBBox();
        var ie = mxClient.IS_IE && mxClient.IS_SVG;
        bbox = new mxRectangle(
          bbox.x,
          bbox.y + (ie ? 0 : 1),
          bbox.width,
          bbox.height + (ie ? 1 : 0)
        );
      } catch (e) {
        // Ignores NS_ERROR_FAILURE in FF if container display is none.
      }
    } else {
      // Computes size if not in document or no getBBox available
      var div = document.createElement("div");

      // Wrapping and clipping can be ignored here
      div.style.lineHeight = mxConstants.ABSOLUTE_LINE_HEIGHT
        ? s.fontSize * mxConstants.LINE_HEIGHT + "px"
        : mxConstants.LINE_HEIGHT;
      div.style.fontSize = s.fontSize + "px";
      div.style.fontFamily = s.fontFamily;
      div.style.whiteSpace = "nowrap";
      div.style.position = "absolute";
      div.style.visibility = "hidden";
      div.style.display = mxClient.IS_QUIRKS ? "inline" : "inline-block";
      div.style.zoom = "1";

      if ((s.fontStyle & mxConstants.FONT_BOLD) == mxConstants.FONT_BOLD) {
        div.style.fontWeight = "bold";
      }

      if ((s.fontStyle & mxConstants.FONT_ITALIC) == mxConstants.FONT_ITALIC) {
        div.style.fontStyle = "italic";
      }

      str = mxUtils.htmlEntities(str, false);
      div.innerHTML = str.replace(/\n/g, "<br/>");

      document.body.appendChild(div);
      var w = div.offsetWidth;
      var h = div.offsetHeight;
      div.parentNode.removeChild(div);

      if (align == mxConstants.ALIGN_CENTER) {
        x -= w / 2;
      } else if (align == mxConstants.ALIGN_RIGHT) {
        x -= w;
      }

      if (valign == mxConstants.ALIGN_MIDDLE) {
        y -= h / 2;
      } else if (valign == mxConstants.ALIGN_BOTTOM) {
        y -= h;
      }

      bbox = new mxRectangle(
        (x + 1) * s.scale,
        (y + 2) * s.scale,
        w * s.scale,
        (h + 1) * s.scale
      );
    }

    if (bbox != null) {
      var n = this.createElement("rect");
      n.setAttribute("fill", s.fontBackgroundColor || "none");
      n.setAttribute("stroke", s.fontBorderColor || "none");
      n.setAttribute("x", Math.floor(bbox.x - 1));
      n.setAttribute("y", Math.floor(bbox.y - 1));
      n.setAttribute("width", Math.ceil(bbox.width + 2));
      n.setAttribute("height", Math.ceil(bbox.height));

      var sw =
        s.fontBorderColor != null ? Math.max(1, this.format(s.scale)) : 0;
      n.setAttribute("stroke-width", sw);

      // Workaround for crisp rendering - only required if not exporting
      if (this.root.ownerDocument == document && mxUtils.mod(sw, 2) == 1) {
        n.setAttribute("transform", "translate(0.5, 0.5)");
      }

      node.insertBefore(n, node.firstChild);
    }
  }
};

/**
 * Function: stroke
 *
 * Paints the outline of the current path.
 */
mxSvgCanvas2D.prototype.stroke = function () {
  this.addNode(false, true);
};

/**
 * Function: fill
 *
 * Fills the current path.
 */
mxSvgCanvas2D.prototype.fill = function () {
  this.addNode(true, false);
};

/**
 * Function: fillAndStroke
 *
 * Fills and paints the outline of the current path.
 */
mxSvgCanvas2D.prototype.fillAndStroke = function () {
  this.addNode(true, true);
};
